<?php

declare(strict_types=1);

namespace App\Repository;

use PDO;
use RuntimeException;

final readonly class WebhookRepository
{
    public function __construct(private PDO $pdo) {}

    /**
     * @return array{id:int,token:string,label:?string,response_status:int,response_content_type:string,response_headers:string,response_body:string,created_at:string,last_accessed_at:?string}
     */
    public function create(?string $label = null): array
    {
        $token = $this->generateUniqueToken();

        $statement = $this->pdo->prepare(
            'INSERT INTO webhooks (token, label) VALUES (:token, :label) RETURNING id, token, label, response_status, response_content_type, response_headers, response_body, created_at, last_accessed_at',
        );
        $statement->bindValue(':token', $token, PDO::PARAM_STR);
        if ($label === null || $label === '') {
            $statement->bindValue(':label', null, PDO::PARAM_NULL);
        } else {
            $statement->bindValue(':label', $label, PDO::PARAM_STR);
        }
        $statement->execute();

        /** @var array{ id:int, token:string, label:?string, response_status:int, response_content_type:string, response_headers:string, response_body:string, created_at:string, last_accessed_at:?string}|false $row */
        $row = $statement->fetch();

        if ($row === false) {
            throw new RuntimeException('Failed to create webhook.');
        }

        return $row;
    }

    /**
     * @return list<array{id:int,token:string,label:?string,created_at:string,last_accessed_at:?string}>
     */
    public function list(int $limit = 50): array
    {
        $statement = $this->pdo->prepare(
            'SELECT id, token, label, created_at, last_accessed_at FROM webhooks ORDER BY created_at DESC LIMIT :limit',
        );
        $statement->bindValue(':limit', $limit, PDO::PARAM_INT);
        $statement->execute();

        /** @var list<array{id:int,token:string,label:?string,created_at:string,last_accessed_at:?string}> $rows */
        $rows = $statement->fetchAll();

        return $rows;
    }

    /**
     * @return array{id:int,token:string,label:?string,response_status:int,response_content_type:string,response_headers:string,response_body:string,created_at:string,last_accessed_at:?string}|null
     */
    public function findByToken(string $token): array|null
    {
        $statement = $this->pdo->prepare(
            'SELECT id, token, label, response_status, response_content_type, response_headers, response_body, created_at, last_accessed_at FROM webhooks WHERE token = :token',
        );
        $statement->bindValue(':token', $token, PDO::PARAM_STR);
        $statement->execute();

        /** @var array{id:int,token:string,label:?string,response_status:int,response_content_type:string,response_headers:string,response_body:string,created_at:string,last_accessed_at:?string}|false $row */
        $row = $statement->fetch();

        return $row === false ? null : $row;
    }

    public function updateResponse(
        int $webhookId,
        int $status,
        string $contentType,
        string $headers,
        string $body,
    ): void {
        $statement = $this->pdo->prepare(
            'UPDATE webhooks SET response_status = :status, response_content_type = :content_type, response_headers = :headers, response_body = :body WHERE id = :id',
        );
        $statement->bindValue(':status', $status, PDO::PARAM_INT);
        $statement->bindValue(':content_type', $contentType, PDO::PARAM_STR);
        $statement->bindValue(':headers', $headers, PDO::PARAM_STR);
        $statement->bindValue(':body', $body, PDO::PARAM_STR);
        $statement->bindValue(':id', $webhookId, PDO::PARAM_INT);
        $statement->execute();
    }

    public function delete(int $webhookId): void
    {
        $statement = $this->pdo->prepare('DELETE FROM webhooks WHERE id = :id');
        $statement->bindValue(':id', $webhookId, PDO::PARAM_INT);
        $statement->execute();
    }

    public function touch(int $webhookId): void
    {
        $statement = $this->pdo->prepare(
            'UPDATE webhooks SET last_accessed_at = CURRENT_TIMESTAMP WHERE id = :id',
        );
        $statement->bindValue(':id', $webhookId, PDO::PARAM_INT);
        $statement->execute();
    }

    private function generateUniqueToken(int $attempt = 0): string
    {
        $token = $this->generateToken();

        if (!$this->tokenExists($token)) {
            return $token;
        }

        if ($attempt > 5) {
            throw new RuntimeException('Unable to generate unique webhook token.');
        }

        return $this->generateUniqueToken($attempt + 1);
    }

    private function tokenExists(string $token): bool
    {
        $statement = $this->pdo->prepare('SELECT 1 FROM webhooks WHERE token = :token');
        $statement->bindValue(':token', $token, PDO::PARAM_STR);
        $statement->execute();

        return (bool) $statement->fetchColumn();
    }

    private function generateToken(): string
    {
        $bytes = random_bytes(16);

        // Format as RFC 4122 version 4 UUID for readability.
        $bytes[6] = chr((ord($bytes[6]) & 0x0f) | 0x40);
        $bytes[8] = chr((ord($bytes[8]) & 0x3f) | 0x80);

        return vsprintf('%s%s-%s-%s-%s-%s%s%s', str_split(bin2hex($bytes), 4));
    }
}
